Skip to content

Automated Test: enhance-passkey-authentication-flow #321

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features

    • Enhanced re-authentication support for users with passkey credentials
    • Improved conditional passkey handling during username and password form authentication
  • Bug Fixes

    • Corrected WebAuthn authenticator display logic in re-authentication flows
    • Fixed form configuration and field visibility during re-authenticated sessions
  • Tests

    • Added comprehensive test coverage for passkey re-authentication scenarios with discoverable and external security keys

mposolda and others added 2 commits July 22, 2025 10:00
closes #41242
closes #41008

Signed-off-by: mposolda <mposolda@gmail.com>
This change modifies the method signature to require a UserModel parameter
for proper user context validation during conditional passkey checks.
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This pull request refactors WebAuthn and passkey authentication handling in Keycloak by extracting reusable helper methods for reauthentication logic, making the reauthentication flag public, and introducing comprehensive test coverage for passwordless reauthentication scenarios with discoverable and external keys.

Changes

Cohort / File(s) Summary
Authenticator Base Infrastructure
services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java, services/src/main/java/org/keycloak/authentication/authenticators/util/AuthenticatorUtils.java
Exposes USER_SET_BEFORE_USERNAME_PASSWORD_AUTH constant as public and extracts reauthentication form setup logic into a new utility method setupReauthenticationInUsernamePasswordFormError() that hides the username field and disables registration when a user was set before password authentication.
Username/Password Authentication
services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java, services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
Adds conditional passkeys checks to prevent form skipping when conditional passkeys are enabled, and introduces isConditionalPasskeysEnabled(UserModel user) helper method to centralize passkey enablement logic instead of direct webauthn presence checks.
WebAuthn Authenticators
services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnAuthenticator.java, services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnConditionalUIAuthenticator.java
Extracts user presence checks into shouldShowWebAuthnAuthenticators() helper method; WebAuthnConditionalUIAuthenticator also integrates reauthentication form setup on error and disables WebAuthn authenticator display for conditional passkey flows.
Reauthentication Tests
testsuite/integration-arquillian/.../PasskeysUsernameFormTest.java, testsuite/integration-arquillian/.../PasskeysOrganizationAuthenticationTest.java
Adds new reauthentication test method webauthnLoginWithDiscoverableKey_reauthentication() to both test classes, validating passkeys authentication during OIDC reauthentication with prompt=login.
Extended Passwordless Tests
testsuite/integration-arquillian/.../PasskeysUsernamePasswordFormTest.java
Introduces comprehensive test coverage for external key reauthentication, mixed password/passkey flows, and users without passkeys, including new helper method setPasswordlessPolicyForExternalKey() and three new test methods for various reauthentication scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Helpers hop and tests now bloom,
Reauthentication clears the room,
Passkeys dance in conditional light,
Forms now hide what should take flight,
Clean refactored logic shines so bright!

🚥 Pre-merge checks | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is generic boilerplate text stating it was automatically created and is a batch pull request, lacking any meaningful detail about what changes were made, why, or how they affect the codebase. Provide a substantive description explaining the key changes, motivation, and impact, such as what authenticator refactoring was done, why re-authentication flows were improved, and which test scenarios were added.
Title check ❓ Inconclusive The title does not clearly describe the primary change. While it mentions 'enhance-passkey-authentication-flow', this is vague and does not reflect the actual code changes which involve refactoring authenticator logic, extracting utility methods, and adding re-authentication support. Revise the title to be more specific about the main change, such as 'Refactor WebAuthn and username/password authentication for conditional passkeys support' or similar, clearly indicating the architectural improvements made.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch enhance-passkey-authentication-flow

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java (1)

336-421: ⚠️ Potential issue | 🟡 Minor

Fix typo on Line 413: "Succesful" → "Successful".

The event handling pattern in this test is correct. The LOGIN event generated at line 363 is explicitly cleared by events.clear() on line 383 before the next sequence of assertions begins. This reset-via-clear approach is intentional and safe—expectLogin().assertEvent() does consume (remove) events from the queue via poll().

🤖 Fix all issues with AI agents
In
`@services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java`:
- Line 47: The call to isConditionalPasskeysEnabled() in UsernameForm is missing
the required UserModel argument; update the condition in the UsernameForm class
to call isConditionalPasskeysEnabled(context.getUser()) (matching how
UsernamePasswordForm uses it) so the method receives the current user from
context.getUser().

In
`@services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java`:
- Around line 136-141: The error challenge path currently calls
isConditionalPasskeysEnabled(context.getUser()) which is false when
context.getUser() is null, removing passkey UI after failed username submission;
update challenge(AuthenticationFlowContext context, String error, String field)
to fall back when context.getUser() is null by reading the attempted username
from the authentication session (e.g.
context.getAuthenticationSession().getAuthNote("username") or the equivalent
constant), resolve the UserModel via
context.getSession().users().getUserByUsername(context.getRealm(),
attemptedUsername), then call isConditionalPasskeysEnabled(resolvedUser) and if
true invoke webauthnAuth.fillContextForm(context) before returning
super.challenge(...).
- Around line 113-117: The call to isConditionalPasskeysEnabled() in
UsernameForm has the wrong signature; update the call to pass the current user
and guard for null: use context.getUser() in the condition (check
context.getUser() != null) and call
isConditionalPasskeysEnabled(context.getUser()) (negate as intended in the
original conditional) so the UsernameForm call matches the protected boolean
isConditionalPasskeysEnabled(UserModel user) in UsernamePasswordForm.
🧹 Nitpick comments (3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernameFormTest.java (1)

261-302: Consider extracting shared setup with webauthnLoginWithDiscoverableKey.

Lines 261–302 largely duplicate the setup and first login from webauthnLoginWithDiscoverableKey (Lines 112–153). A shared helper (e.g., loginWithDiscoverableKeySetup()) returning the UserRepresentation could reduce duplication across both tests.

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysOrganizationAuthenticationTest.java (1)

291-296: Nit: consider asserting loginPage.assertCurrent() or WebAuthn page state before the auto-login.

Between oauth.openLoginForm() (Line 292) and appPage.assertCurrent() (Line 295), the discoverable key auto-authenticates so there's no visible intermediate page. This is fine for the happy path, but if the auto-login silently fails in the future, the test will produce a confusing appPage.assertCurrent() failure with no indication of what page was actually displayed. A brief comment or a try-catch with a diagnostic message could help future debugging — though this is a very minor point.

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java (1)

291-296: Element-absence checks are overly complex and catch too broadly.

The current pattern has unnecessary complexity:

  1. driver.findElement() never returns null, so assertThat(..., nullValue()) always fails if the element is found (throwing AssertionError, an Error, not Exception). If the element is absent, findElement throws NoSuchElementException before the assertion is reached.
  2. Catching Exception is too broad — it would silently swallow unexpected exceptions (e.g., StaleElementReferenceException, WebDriverException).

The idiomatic Selenium pattern is simpler:

♻️ Suggested simplification (apply to both blocks at lines 291-296 and 312-317)
-            try {
-                MatcherAssert.assertThat(driver.findElement(By.xpath("//form[`@id`='webauth']")), nullValue());
-                fail("Not expected to have webauthn button");
-            } catch (Exception nsee) {
-                // expected
-            }
+            try {
+                driver.findElement(By.xpath("//form[`@id`='webauth']"));
+                fail("Not expected to have webauthn button");
+            } catch (NoSuchElementException nsee) {
+                // expected
+            }

This catches only the expected NoSuchElementException, and fail() is reached only if the element is unexpectedly present.

Also applies to: 312-317

@@ -44,7 +44,7 @@ public UsernameForm(KeycloakSession session) {

@Override
public void authenticate(AuthenticationFlowContext context) {
if (context.getUser() != null) {
if (context.getUser() != null && !isConditionalPasskeysEnabled()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all definitions and usages of isConditionalPasskeysEnabled across the codebase
rg -nP 'isConditionalPasskeysEnabled\s*\(' --type=java -B2 -A2

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 3116


Compilation error: isConditionalPasskeysEnabled() called without required UserModel argument.

The parent class UsernamePasswordForm defines isConditionalPasskeysEnabled(UserModel user) at line 160, but line 47 invokes it without arguments. This will fail to compile. Based on correct usage patterns in UsernamePasswordForm at lines 115 and 137, the call should pass context.getUser().

Proposed fix
-        if (context.getUser() != null && !isConditionalPasskeysEnabled()) {
+        if (context.getUser() != null && !isConditionalPasskeysEnabled(context.getUser())) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (context.getUser() != null && !isConditionalPasskeysEnabled()) {
if (context.getUser() != null && !isConditionalPasskeysEnabled(context.getUser())) {
🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java`
at line 47, The call to isConditionalPasskeysEnabled() in UsernameForm is
missing the required UserModel argument; update the condition in the
UsernameForm class to call isConditionalPasskeysEnabled(context.getUser())
(matching how UsernamePasswordForm uses it) so the method receives the current
user from context.getUser().

Comment on lines +113 to 117
}
// setup webauthn data when passkeys enabled
if (isConditionalPasskeysEnabled(context.getUser())) {
webauthnAuth.fillContextForm(context);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ENABLE_WEBAUTHN_CONDITIONAL_UI usage
echo "=== ENABLE_WEBAUTHN_CONDITIONAL_UI usage ==="
rg -n 'ENABLE_WEBAUTHN_CONDITIONAL_UI' --type=java -C 2

echo ""
echo "=== isConditionalPasskeysEnabled usage ==="
rg -n 'isConditionalPasskeysEnabled' --type=java -C 2

echo ""
echo "=== fillContextForm usage in relevant files ==="
rg -n 'fillContextForm' --type=java -C 2 | head -100

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 11070


🏁 Script executed:

#!/bin/bash
# Find the WebAuthnConditionalUIAuthenticator and WebAuthnAuthenticator files
echo "=== Finding WebAuthnConditionalUIAuthenticator ==="
fd -t f -n 'WebAuthnConditionalUIAuthenticator' --type=java

echo ""
echo "=== Finding WebAuthnAuthenticator ==="
fd -t f -n 'WebAuthnAuthenticator' --type=java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 655


🏁 Script executed:

#!/bin/bash
# Look for the test file mentioned
echo "=== Finding PasskeysUsernameFormTest ==="
fd -t f -n 'PasskeysUsernameFormTest'

echo ""
echo "=== Finding UsernamePasswordForm ==="
fd -t f -n 'UsernamePasswordForm'

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 644


🏁 Script executed:

#!/bin/bash
# Find and view UsernameForm.java
rg -l 'class UsernameForm' --type=java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 277


🏁 Script executed:

#!/bin/bash
# View the content of UsernameForm.java
rg -A 100 'public class UsernameForm' --type=java | head -150

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 9325


🏁 Script executed:

#!/bin/bash
# Check what isConditionalPasskeysEnabled methods exist in UsernameForm
rg -B2 -A5 'isConditionalPasskeysEnabled' services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 512


🏁 Script executed:

#!/bin/bash
# Find and view test file to understand which authenticator is tested
find . -name '*PasskeysUsernameFormTest*' -type f

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 221


🏁 Script executed:

#!/bin/bash
# View full UsernameForm.java
cat -n services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 5223


🏁 Script executed:

#!/bin/bash
# View PasskeysUsernameFormTest.java
cat -n ./testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernameFormTest.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 17092


🏁 Script executed:

#!/bin/bash
# Search for all isConditionalPasskeysEnabled method definitions
echo "=== All isConditionalPasskeysEnabled methods ==="
rg -n 'protected.*boolean.*isConditionalPasskeysEnabled' --type=java -A 2

echo ""
echo "=== Parent class of UsernamePasswordForm ==="
rg -n 'public class UsernamePasswordForm' --type=java -A 5

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2403


🏁 Script executed:

#!/bin/bash
# Check UsernamePasswordForm full authenticate method to trace user handling
rg -n 'public void authenticate\|if \(context.getUser' services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java -A 1 | head -40

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Search for how validateUser might set context user
rg -n 'validateUser\|context.setUser' services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java -B 2 -A 2

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Find AbstractUsernameFormAuthenticator
fd -t f 'AbstractUsernameFormAuthenticator' --type=java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 258


🏁 Script executed:

#!/bin/bash
# View AbstractUsernameFormAuthenticator
cat -n services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 13732


🏁 Script executed:

#!/bin/bash
# Search for isConditionalPasskeysEnabled() with no parameters
rg -n 'isConditionalPasskeysEnabled\(\)' --type=java -B 2 -A 2

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 914


🏁 Script executed:

#!/bin/bash
# Search more thoroughly for all isConditionalPasskeysEnabled methods in the codebase
rg 'isConditionalPasskeysEnabled' -B 1 -A 2

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2605


🏁 Script executed:

#!/bin/bash
# Check the full authenticate method of UsernamePasswordForm
wc -l services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
cat -n services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java | sed -n '80,125p'

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2675


🏁 Script executed:

#!/bin/bash
# Check if there's a parameterless isConditionalPasskeysEnabled in UsernameForm itself
cat -n services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java | grep -A 5 'isConditionalPasskeysEnabled'

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 474


🏁 Script executed:

#!/bin/bash
# Check if there's an override in UsernameForm for the method
rg -n 'class UsernameForm' -A 70 services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java | grep -i 'conditional'

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 165


🏁 Script executed:

#!/bin/bash
# Try to compile or check for compilation errors - search for all methods in UsernameForm
cat -n services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 5223


🏁 Script executed:

#!/bin/bash
# Final verification: search for any parameterless isConditionalPasskeysEnabled definition
rg 'protected.*boolean.*isConditionalPasskeysEnabled\(\)' --type f

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Check if there's any method added in the changed lines that might define a parameterless version
cat -n services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java | sed -n '150,164p'

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 547


🏁 Script executed:

#!/bin/bash
# Verify the entire UsernamePasswordForm class to see all method definitions
wc -l services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java

echo ""
echo "=== Checking for all isConditionalPasskeysEnabled definitions in the entire file ==="
grep -n 'isConditionalPasskeysEnabled' services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 483


Method signature mismatch prevents compilation: isConditionalPasskeysEnabled() in UsernameForm requires a parameter.

Line 47 of UsernameForm calls isConditionalPasskeysEnabled() with no arguments, but the only definition in UsernamePasswordForm at line 160 is protected boolean isConditionalPasskeysEnabled(UserModel user) which requires a UserModel parameter. UsernameForm does not override this method, so the call will fail to compile.

Change line 47 in UsernameForm to: if (context.getUser() != null && !isConditionalPasskeysEnabled(context.getUser()))

🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java`
around lines 113 - 117, The call to isConditionalPasskeysEnabled() in
UsernameForm has the wrong signature; update the call to pass the current user
and guard for null: use context.getUser() in the condition (check
context.getUser() != null) and call
isConditionalPasskeysEnabled(context.getUser()) (negate as intended in the
original conditional) so the UsernameForm call matches the protected boolean
isConditionalPasskeysEnabled(UserModel user) in UsernamePasswordForm.

Comment on lines 136 to 141
protected Response challenge(AuthenticationFlowContext context, String error, String field) {
if (context.getUser() == null && webauthnAuth != null && webauthnAuth.isPasskeysEnabled()) {
// setup webauthn data when the user is not already selected
if (isConditionalPasskeysEnabled(context.getUser())) {
// setup webauthn data when possible
webauthnAuth.fillContextForm(context);
}
return super.challenge(context, error, field);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same concern applies to the error challenge path.

When challenge(context, error, field) is invoked after a failed login attempt where the user was not identified, context.getUser() will be null, and isConditionalPasskeysEnabled(null) returns false. The passkey conditional UI won't be repopulated on the error form, removing the passkey option after a failed username submission.

🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java`
around lines 136 - 141, The error challenge path currently calls
isConditionalPasskeysEnabled(context.getUser()) which is false when
context.getUser() is null, removing passkey UI after failed username submission;
update challenge(AuthenticationFlowContext context, String error, String field)
to fall back when context.getUser() is null by reading the attempted username
from the authentication session (e.g.
context.getAuthenticationSession().getAuthNote("username") or the equivalent
constant), resolve the UserModel via
context.getSession().users().getUserByUsername(context.getRealm(),
attemptedUsername), then call isConditionalPasskeysEnabled(resolvedUser) and if
true invoke webauthnAuth.fillContextForm(context) before returning
super.challenge(...).

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant